﻿/*
 * Copyright 2011 Edward L. Stauff.                           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "TypeMappingDictionary.cs", is part of the "Stauffware.Common.Xml.Xerialization" 
 * library.  "Stauffware.Common.Xml.Xerialization" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Xml.Xerialization" is distributed in the hope that it will be 
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common.Xml.Xerialization".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using System.Reflection;
using Stauffware.Common.Core;

namespace Stauffware.Common.Xml.Xerialization
{
	/// <summary>
	/// Maintains a collection of XerialTypeMapping objects, which can be referenced by 
	/// name or by the target Type.
	/// </summary>

	public class TypeMappingDictionary
	{
		// these dictionaries contains the same set of values,but keyed differently
		private readonly Dictionary<Type, XerialTypeMapping> TypeDict = new Dictionary<Type, XerialTypeMapping>();
		private readonly Dictionary<string, XerialTypeMapping> NameDict;

		private readonly XerialContext Context;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Gets the type mapping for the given Type, or null if there isn't one.
		/// </summary>

		public XerialTypeMapping this[Type type]
		{
			get
			{
				XerialTypeMapping xtm;
				if (this.TypeDict.TryGetValue(type, out xtm))
					return xtm;
				else return null;
			}
		}

		/// <summary>
		/// Gets the type mapping with the given name, or null if there isn't one.
		/// Ignores case according to XerialContext.IgnoreXmlCase.
		/// </summary>
		public XerialTypeMapping this[string xmlName]
		{
			get
			{
				XerialTypeMapping xtm;
				if (this.NameDict.TryGetValue(xmlName, out xtm))
					return xtm;
				else return null;
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="context">the context to which this dictionary belongs</param>

		internal TypeMappingDictionary (XerialContext context)
		{
			Require.NotNull(context);
			this.Context = context;
			this.NameDict = new Dictionary<string, XerialTypeMapping>(context.IgnoreXmlCase ?
								StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds one or more type mappings to this dictionary.
		/// If any of the type mappings couldn't be added because of a type or name
		/// conflict, an exception is thrown that describes all the conflicts
		/// (not just the first one encountered).
		/// </summary>

		public void Add (params XerialTypeMapping[] mappings)
		{
			string conflicts = "";
			foreach (XerialTypeMapping tm in mappings)
				this.Add(tm, ref conflicts);

			if (conflicts != "")
				throw new InputRequirement("XerialMappingDictionary already contains XerialTypeMappings for:  " + 
									conflicts.Substring(errorSeparator.Length));
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds mappings for one or more types by calling GetOrAdd for each one.
		/// </summary>

		public void AddIfNeeded (params Type[] types)
		{
			foreach (Type t in types)
				this.GetOrAdd(t);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Gets a type mapping for the given type, creating it if necessary, along with
		/// any referenced types that are needed (see remarks).
		/// Throws an exception if the XerialMappingDictionary doesn't support the Type.
		/// </summary>
		/// <param name="type">
		///		The Type to be added, one of: 
		///		(a) a class or struct with a XerialTypeAttribute,
		///		(b) any type that implements IList, or
		///		(c) any enum.
		/// </param>
		/// <returns>the XerialTypeMapping for the Type; never returns null</returns>
		/// <remarks>
		/// When a type mapping is created for a class or a struct, this routine
		/// recurses on the type for each field and property that will participate
		/// in xerialization.
		/// When a type mapping is created for an IList, this routine recurses on
		/// the list member type.
		/// </remarks>

		public XerialTypeMapping GetOrAdd (Type type)
		{
			XerialTypeMapping m;
			if (this.TypeDict.TryGetValue(type, out m))
				return m;

			// see if the type has our custom attribute

			XerialTypeAttribute attr = XerialTypeAttribute.GetFrom(type);
			if (attr != null)
			{
				if (type.IsClass || IsStruct(type))
					return this.CreateMapping(type, attr.XmlName, attr);
				else throw new XerialException("found a Type with a XerialTypeAttribute, but it's not a class: " +
												type.FullName);
			}

			// see if it's an enum

			if (type.IsEnum)
			{
				XerialEnumMapping em = new XerialEnumMapping(this.Context, type);
				this.Add(em);
				return em;
			}

			// see if we can handle it as some sort of list

			XerialListMapping lm = XerialListMapping.TryCreate(this.Context, type, attr == null ? null : attr.XmlName);
			if (lm != null)
			{
				this.Add(lm);
				return lm;
			}

			if (type.IsClass || IsStruct(type))
				return this.CreateMapping(type, type.Name, null);

			throw new XerialException(type.FullName + " cannot be mapped automatically.  " + 
					"You must tag that type with a " + typeof(XerialTypeAttribute).Name + ", " + 
					"or handle it by implementing a subclass of " + typeof(XerialTypeMapping).Name + 
					" and adding it to the " + typeof(TypeMappingDictionary).Name + ".");
		}

		//-----------------------------------------------------------------------------

		private XerialTypeMapping CreateMapping (Type targetType, string xmlName, XerialTypeAttribute typeAttr )
		{
			if (DebugLogEnabled) LogLine("creating " + (typeAttr == null ? "automatic " : "") +
										 "type mapping for " + targetType.FullName);

			XerialClassMapping cm = new XerialClassMapping(this.Context, targetType, xmlName);
			this.Add(cm);

			// build dependent mappings AFTER adding this one to the dictionary in case of circular references
			cm.BuildXerialGraph(typeAttr);
			return cm;
		}

		//-----------------------------------------------------------------------------

		private static bool IsStruct (Type t)
		{
			return t.IsValueType && (!t.IsPrimitive) && (!t.IsEnum) && (!t.Namespace.StartsWith("System"));
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Tries to adds the given type mapping to this dictionary.
		/// </summary>
		/// <param name="tm">the type mapping to add</param>
		/// <param name="errors">
		///		If tm.TargetType is already in this dictionary, the type name is appended to this string.
		///		If tm.XmlName is already in this dictionary, the name is appended, in quotes, to this string.
		/// </param>
		/// <returns>true on success, false on error</returns>

		private bool Add (XerialTypeMapping tm, ref string errors)
		{
			Require.NotNull(tm);

			//if (!(tm.AllowsXmlAttr || tm.AllowsXmlElmnt || tm.AllowsXmlText))
			//{
			//    errors += errorSeparator + tm.TargetType.FullName + " doesn't allow any XML kind!";
			//    return false;
			//}

			bool ok = true;

			if (this.TypeDict.ContainsKey(tm.TargetType))
			{
				errors += errorSeparator + tm.TargetType.FullName + " already exists";
				ok = false;
			}

			if (this.NameDict.ContainsKey(tm.XmlName))
			{
				errors += ";  \"" + tm.XmlName + "\" already exists";
				ok = false;
			}

			if (ok)
			{
				this.TypeDict.Add(tm.TargetType, tm);
				this.NameDict.Add(tm.XmlName, tm);
			}

			Assert.Condition(this.TypeDict.Count == this.NameDict.Count,
						"dictionary count mismatch: TypeDict=" + TypeDict.Count.ToString() + 
												 ", NameDict=" + NameDict.Count.ToString());

			return ok;
		}

		private const string errorSeparator = "; ";

		//-----------------------------------------------------------------------------
		/// <summary>
		/// For each exported type in the given assembly, a type mapping is created if
		/// the type has a XerialTypeAttribute.
		/// </summary>

		public void Add (Assembly asm)
		{
			foreach (Type t in asm.GetExportedTypes())
				if (XerialTypeAttribute.GetFrom(t) != null)
					this.AddIfNeeded(t);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds mappings for standard primitive types (string, boolean, numbers, etc.)
		/// </summary>

		internal void AddStandardMappings ()
		{
			this.Add(new NullClassMapping(this.Context, typeof(System.Object)),
					 new NullClassMapping(this.Context, typeof(System.ValueType)),
					 new XerialStringMapping(this.Context),
					 new XerialCharMapping(this.Context),
					 new XerialBooleanMapping(this.Context),
					 new XerialDateTimeMapping(this.Context),
					 new XerialTimeSpanMapping(this.Context),
					 new XerialNumberMapping<System.SByte>(this.Context),
					 new XerialNumberMapping<System.Int16>(this.Context),
					 new XerialNumberMapping<System.Int32>(this.Context),
					 new XerialNumberMapping<System.Int64>(this.Context),
					 new XerialNumberMapping<System.Byte>(this.Context),
					 new XerialNumberMapping<System.UInt16>(this.Context),
					 new XerialNumberMapping<System.UInt32>(this.Context),
					 new XerialNumberMapping<System.UInt64>(this.Context),
					 new XerialNumberMapping<System.Decimal>(this.Context),
					 new XerialNumberMapping<System.Single>(this.Context),
					 new XerialNumberMapping<System.Double>(this.Context) );

			foreach (XerialTypeMapping m in this.NameDict.Values)
				m.IsInternal = true;
		}

		#region Debugging stuff

		/// <summary>
		/// Returns whether the debug log is enabled.
		/// </summary>
		protected bool DebugLogEnabled { get { return this.Context.DebugLog != null; } }

		//-----------------------------------------------------------------------------
		/// <summary> Writes a line of debugging output. </summary>

		protected void LogLine (string str)
		{
			this.Context.DebugLog.WriteLine(str);
		}

		//-----------------------------------------------------------------------------
		/// <summary> Dumps the guts of the object.  For debugging only. </summary>

		public void Dump (System.IO.TextWriter tw, bool includeInternal)
		{
			tw.WriteLine("Type Mappings");
			tw.WriteLine();

			List<string> names = new List<string>(this.NameDict.Keys);
			names.Sort();
			foreach (string name in names)
			{
				XerialTypeMapping mm = this.NameDict[name];
				if (mm.IsInternal && !includeInternal) continue;
				mm.Dump(tw);
				tw.WriteLine();
			}
		}

		//-----------------------------------------------------------------------------

		#endregion
	}

	//#################################################################################
	/// <summary>
	/// Special mapping for System.Object and System.ValueType, 
	/// to avoid handling them as special cases elsewhere.
	/// This mapping pretty much does nothing.
	/// </summary>

	public class NullClassMapping : XerialClassMapping
	{
		/// <inherit/>
		public override bool AllowsXmlElmnt { get { return false; } }
		/// <inherit/>
		public override bool AllowsXmlAttr { get { return false; } }
		/// <inherit/>
		public override bool AllowsXmlText { get { return false; } }

		/// <summary> constructor </summary>
		public NullClassMapping (XerialContext context, Type type)
			: base (context, type, type.FullName) { }

		/// <inherit/>
		public override void BuildXerialGraph (XerialTypeAttribute classAttr) { }
		/// <inherit/>
		protected override void WriteAttributeMembers (object o, XmlWriter w) { }
		/// <inherit/>
		protected override void WriteChildElementMembers (object o, XmlWriter w) { }
		/// <inherit/>
		protected override XerialMemberMapping GetElementMemberMapping (string elName) { return null; }
		/// <inherit/>
		protected override XerialMemberMapping GetAttrMemberMapping (string attrName) { return null; }
	}

}
